R-Version: [Default] [32-bit] C:\Program Files\R\R-4.1.0
TODO: Einführungstext ändern
In folgendem Notebook werden anhand des MovieLense Datensatzes aus dem Paket RecommenderLab verschiedene Recommender erstellt. Es werden verschiedene Recommender und verschiedene Ähnlichkeiten verwendet, um diese zu vergleichen und auszuwerten. Ziel ist es, ein möglichst guter Recommender zu erstellen und zu verstehen wie dieser funktioniert. Zudem soll verstanden werden wie dieser bewertet wird und was in diesem Falle ein ‘guter’ Recommender bedeutet.
Dieses Notebook konzentriert sich auf Erkenntnisse von Auswertungen und Vergleichen. Um eine bessere Übersicht zu erhalten wurden grosse, sich widerholende Codes im Helperfile helper.R ausgelagert.
movies_binary <- movies %>% mutate(rating = ifelse(rating > 3, 1, 0))
movies_wider <- pivot_wider(movies_binary, id_cols = user, names_from = item, values_from = rating)
rownames(movies_wider) <- movies_wider$user
movies_wider['user'] <- NULL
user_movie_matrix <- as.matrix(movies_wider)
user_liked_item <- as(binarize(MovieLense, minRating = 4), 'matrix') * 1
user_liked_item[1:3, 1:3]
Toy Story (1995) GoldenEye (1995) Four Rooms (1995)
1 1 0 1
2 1 0 0
3 0 0 0
Für die Binäre User-Liked Matrix setzten wir die Grenze für ein gutes Rating bei >4. Also alle Filme, welche mit einem Rating von 3 oder weniger bewertet wurden, werden als schlecht bewertet definiert (also 0), wobei Filme mit Bewertungen von 4 oder 5 als gut bewertet definiert sind (1).
print_dim(user_liked_item)
Dimension: ( 943 1664 )
Es sind 943 Users und 1664 bewertete Filme vorhanden. Dies entspricht der Dimension der Ursprungsdaten.
genres <- MovieLenseMeta
genres <- genres %>% select("title",'unknown':'Western')
rownames(genres) <- genres$title
genres['title'] <- NULL
movie_genre_matrix <- as.matrix(genres)
movie_genre_matrix[1:4,1:3]
unknown Action Adventure
Toy Story (1995) 0 0 0
GoldenEye (1995) 0 1 1
Four Rooms (1995) 0 0 0
Get Shorty (1995) 0 1 0
print_dim(movie_genre_matrix)
Dimension: ( 1664 19 )
In dieser Matrix sind die 1664 Filme einem oder mehreren von 19 genres zugeordnet.
nr_diff_movies <- user_liked_item %*% movie_genre_matrix
nr_diff_movies <- as.data.frame(nr_diff_movies)
nr_diff_movies_mean <- rownames_to_column(nr_diff_movies)
nr_diff_movies_mean <- pivot_longer(nr_diff_movies_mean, cols = !rowname, names_to = 'genre', values_to = 'count')
nr_diff_movies_mean <- nr_diff_movies_mean %>% group_by(genre) %>% summarize(count = mean(count))
nr_diff_movies_mean %>% mutate(genre = fct_reorder(genre, count)) %>%
ggplot(aes(x = genre, y = count)) +
geom_col(fill = 'steelblue') +
coord_flip() +
scale_y_continuous(expand = c(0,0), limits = c(0, 30)) +
geom_text(aes(label = round(count, 2)), hjust=-0.2, color = 'black') +
labs(
title = "Duchschnittliche Anzahl positiv bewerteter Filme pro Genre",
x = element_blank(),
y = "Anzahl",
fill = element_blank()
) +
theme_classic() +
theme(
text = element_text(size = 12),
legend.position = 'bottom'
)
Hier sehen wird die allgemein am häufigsten positiv bewerteten Genres. Drama, Action und Comedy kommen am häufigsten vor. Ein durchschnittlicher Nutzer hat also 25.78 Filme die mit Drama kategorisiert werden mit 4 oder 5 Sternen bewertet. Hier ist jedoch wichtig zu beachten, dass Genres oft in Kombination mit anderen Genres vorkommen, ein Film also mehreren Genres angehört.
movie_genre_profile_matrix <- as.matrix(
apply(
movie_genre_matrix == 1,
1,
function(a) paste0(colnames(movie_genre_matrix)[a], collapse = "-")
)
)
movie_genre_profile_df <- as.data.frame(movie_genre_profile_matrix)
movie_genre_profile_df$items <- rownames(movie_genre_profile_df)
#movie_genre_profile_list <- c(unique(movie_genre_profile_df$V1))
#movie_genre_profile_list <- strsplit(movie_genre_profile_list, "\n")
#length(movie_genre_profile_list)
head(movie_genre_profile_df,3) %>% select('V1')
movie_genre_profile_df <- movie_genre_profile_df %>%
group_by(V1) %>%
summarize(count = n()) %>%
arrange(desc(count))
head_genre_profile <- movie_genre_profile_df %>% head(30)
tail_genre_profile <- anti_join(movie_genre_profile_df, head_genre_profile, by = 'V1')
movie_genre_profile_df <- rbind(head_genre_profile, c('Andere',sum(tail_genre_profile$count)))
movie_genre_profile_df <- movie_genre_profile_df %>%
transform(count = as.numeric(count)) %>%
mutate(highlight = ifelse(V1 == 'Andere', 'yes', 'no'))
Jedem Film wird auf diese Weise die Kombination der Genres zusammengefasst. Toy Story zum Beispiel wird als Genre-Kombination Animation-Children's-Comedy zusammengefasst. Dies macht soweit Sinn.
ggplot(movie_genre_profile_df, aes(x = reorder(V1,count), y = count, fill = highlight)) +
geom_col() +
scale_fill_manual(values = c('yes' = '#B47846', 'no' = 'steelblue'), guide = 'none') +
coord_flip() +
scale_y_continuous(expand = c(0,0), limits = c(0, 450)) +
geom_text(aes(label = count), hjust=-0.2, color = 'black') +
labs(
title = "Verteilung der Filme nach Genre-Kombination",
subtitle = 'Top 30 Kombinationen, von insgesamt 216',
x = element_blank(),
y = "Anzahl Filme",
fill = element_blank()
) +
theme_classic() +
theme(
text = element_text(size = 12),
legend.position = 'bottom'
)
In dem MovieLense Datenset sind insgesamt 216 verschiedene Genreprofile von den Filmen vertreten. Das bedeutet, es sind 216 verschiedene Genrekombinationen aus den 16 gegebenen Genres entstanden. Hier sehen wir also, dass die häufigste Kombination Drama alleine ist. Ganze 370 Filme wurden nur als Dramas kategorisiert.
user_genre_profil <- user_liked_item %*% movie_genre_matrix
user_genre_profil[1:5,1:5]
unknown Action Adventure Animation Children's
1 1 39 17 5 5
2 0 7 3 1 2
3 0 3 2 0 0
4 0 5 2 0 0
5 1 23 14 8 5
In dieser Matrix sind die Anzahl positiv bewerteter Filme pro Genre für jeden User dargestellt.
print_dim(user_genre_profil)
Dimension: ( 943 19 )
Die Matrix hat für alle 943 Nutzer eine Angabe für die 19 verschiedenen Genres.
user_genre_df <- as.data.frame(user_genre_profil)
user_genre_diff <- user_genre_df %>% mutate(across(.cols = everything(), .fns = ~ifelse(.x > 0, 1, 0)))
cat(paste("Users: ", as.character(dim(user_genre_df)[1])),
paste("vollständig identisch:", as.character(dim(user_genre_df)[1] - count(distinct(user_genre_df)))),
paste("binär identisch: ", as.character(dim(user_genre_df)[1] - count(distinct(user_genre_diff)))), sep = '\n')
Users: 943
vollständig identisch: 0
binär identisch: 562
Es gibt bei Betrachtung vollständiger Nutzerprofilen keine identischen Nutzerprofile. Bei binärer Betrachtung sind hingegen fast 2/3 der Nutzerprofile identisch.
A_test.data <- c(1.5,2.5, 1.,0.5)
A_test <- matrix(A_test.data, nrow=2)
A_test
[,1] [,2]
[1,] 1.5 1.0
[2,] 2.5 0.5
B_test.data <- c(0.5,1., 1.5,2.)
B_test <- matrix(B_test.data, nrow=2)
B_test
[,1] [,2]
[1,] 0.5 1.5
[2,] 1.0 2.0
result <- calc_cos_similarity_twomtrx(A_test, B_test)
if((dim(result) == dim(B_test)) && (dim(result) == dim(A_test))) {
print("dimensions match")
} else {
print("dimensions do not match")
}
[1] "dimensions match"
check.data <- c(0.79, 0.50, 0.87, 0.61)
check <- matrix(check.data, nrow=2)
if(max(abs(check - result)) < 1e-2){
print("check match")
} else{
print("check differs from result")
}
[1] "check match"
In diesem Beispiel wurde für zwei 2x2 Matrizen mit zufällig gewählten Werten die cosine similarity berechnet. Diese Berechnung wurde ebenfalls von Hand gemacht und mit der Implementierung abgegelichen. Zusätzlich wurden die Dimensionen der Inputmatrizen mit der resultierenden Matrix abgeglichen. Die Berechnung der Cosine Similarity ist somit korrekt.
cat(
print_dim(user_genre_profil),
print_dim(movie_genre_matrix)
)
Dimension: ( 943 19 )
Dimension: ( 1664 19 )
similarity <- calc_cos_similarity_twomtrx(user_genre_profil, movie_genre_matrix)
Anschliessend wird die Cosine Similarity der user-genre und movie-genre Matrixberechnet. Für die Berechnung muss die zweite Matrix transponiert werden, damit für die Matrix Multiplikation die Anzahl Spalten der ersten Matrix der Anzahl Zeilen der zweiten übereinstimmt.
similarity[1:3,1:3]
Toy Story (1995) GoldenEye (1995) Four Rooms (1995)
1 0.2927832 0.4267688 0.2578553
2 0.2817181 0.3004993 0.1951800
3 0.1549193 0.4647580 0.3577709
print_dim(similarity)
Dimension: ( 943 1664 )
Wie die Dimension schon erahnen lässt, ist in dieser Matrix die Similarity zwischen Usern und Filmen abgebildet. Die Matrix gibt also für alle 943 User eine Cosine Similarity mit jedem der 1664 Filme an.
summary(c(similarity))
Min. 1st Qu. Median Mean 3rd Qu. Max. NA's
0.0000 0.2300 0.4070 0.4098 0.5919 0.9768 1664
Die similarities liegen zwischen 0 und 1, was bei Betrachtung der ausschliesslich positiven Bewertung Sinn ergibt. Da das Minimum bei 0 liegt, gibt es user-item Kombinationen welche keine Ähnlichkeiten aufweisen. Das Maximum von 0.9768 bedeutet, dass es sehr hohe Ähnlichkeiten zwischen User- und Film-Kombinationen gibt. Es gibt jedoch keine genau gleiche Kombination im gesamten Datensatz. Interessant ist, dass 1664 NA Werte bestehen. Dies hat zu bedeuten, dass es einen User gibt, welcher alle Filme negativ bewertet hat. Für diesen User können wir so keine Empfehlungen generieren.
cat(which(is.na(similarity), arr.ind = T)[1])
685
Wie hier gezeigt, ist dies der Nutzer 685.
similarity_df <- as.data.frame(similarity)
similarity_df <- pivot_longer(similarity_df, cols=colnames(similarity_df)) %>% na.omit(value)
plot_similarity(similarity_df, "Alle 943 User")
Es fällt auf, dass sehr hohe Ähnlichkeiten sehr selten vorkommen. Auch auffallend ist, dass es eine Spitze bei Ähnlichkeiten von 0 gibt, also Filme welche überhaubt nicht zu einem Nutzer passen und demnach eine Cosine-Similarity von 0 besitzen. Auf diesem Diagramm ist es nicht all zu deutlich zu sehen, dass all diese Werte tatsächlich 0 sind, doch genau dies ist bei genauerem Betrachten der Daten aufgefallen. Ansonsten sind die Ähnlichkeiten relativ gleichmässig verteilt. Wobei am meisten Ähnlichkeiten ca. rund um 0.4 liegen.
plot_sim(similarity, "Cosinus similarity zwischen user-genre und movie-genre")
Bei der Visualisierung aller similarities wird sichtbar, dass sich die Farbunterschiede eher hoizontal ausprägen. Dies bedeutet, dass der Einfluss von Nutzern, die generell besser oder generell schlechter bewerten höher ist als die similarities zwischen den Filmen.
selection <- as.data.frame(similarity)[c(241, 414, 477, 526, 640, 710), ]
movies <- colnames(selection)
selection$users <- rownames(selection)
selection_long <- selection %>% pivot_longer(cols = all_of(movies))
p1 <- plot_similarity(selection_long %>% filter(users == 241), "User 241")
p2 <- plot_similarity(selection_long %>% filter(users == 414), "User 414")
p3 <- plot_similarity(selection_long %>% filter(users == 477), "User 477")
p4 <- plot_similarity(selection_long %>% filter(users == 526), "User 526")
p5 <- plot_similarity(selection_long %>% filter(users == 640), "User 640")
p6 <- plot_similarity(selection_long %>% filter(users == 710), "User 710")
grid.arrange(p1, p2, p3, p4, p5, p6, ncol = 2, nrow = 3)
Hier wird nochmals sichtbar, dass das die similarities pro user sehr unterschiedlich sein können. Nummer 640 hat beispielsweise eine grössere similarity Ausprägung bei etwa 0.4. Häufig sind Buckel zu erkennen, was bedeutet dass Nutzer gewisse genres besser oder schlechter bewerten.
movies_masked <- movies_wider
movies_masked[-1][movies_masked[-1] == 1] <- 0
movies_masked[is.na(movies_masked)] <- 1
movies_masked
defined_user <- c(5, 25, 50, 150)
defined_user
[1] 5 25 50 150
rowSums(movies_masked[defined_user, -1])
[1] 1489 1586 1640 1633
Hier zu sehen sind die anzahl nicht bewerteter filme pro user
rowsums_masked <- rowSums(movies_masked[, -1])
summary(rowsums_masked)
Min. 1st Qu. Median Mean 3rd Qu. Max.
928 1516 1599 1558 1631 1645
rating_matrix <- user_movie_matrix * movies_masked
dim(rating_matrix)
[1] 943 1664
rating_matrix